#!/usr/bin/env bash
# Copyright (c) 2022 maminjie <canpool@163.com>
# SPDX-License-Identifier: MulanPSL-2.0


readonly WORK_DIR="$PWD"
readonly TMP_DIR="$WORK_DIR/tmp_$(date +%Y%m%d%H%M%S)"
readonly OUT_DIR="$WORK_DIR/output"

SIZE_THRESHOLD=0

usage() {
printf "usage: bash $0 DIR1 DIR2

compare rpms of different versions of the same software

params:
    DIR - the directory of RPMs
\n"
}


init() {
    [ -d "$OUT_DIR" ] && rm -rf $OUT_DIR
    mkdir -p $OUT_DIR/BIN/list
    mkdir -p $OUT_DIR/BIN/change
    mkdir -p $OUT_DIR/RPM/list
    mkdir -p $OUT_DIR/RPM/change

    mkdir -p $TMP_DIR
}

deinit() {
    [ -d "$TMP_DIR" ] && rm -rf $TMP_DIR
}

# prepare DIR TARGET
prepare() {
    local dir="$1"
    local P="$2"
    local rpm_dir="$TMP_DIR/$P/rpm"
    local rpm_list="$TMP_DIR/$P/list"
    local rely_list="$rpm_dir/${P}_rely_list"
    mkdir -p "$rpm_dir"
    cp $dir/*.rpm $rpm_dir &>/dev/null
    ls $rpm_dir > $rpm_list
    if [ ! -s "$rpm_list" ]; then
        echo "WARNING: no rpms in $dir"
        return 1
    fi
    local OLD_IFS=$IFS
    IFS=$'\n'

    local bin_base_list="$OUT_DIR/BIN/list/${P}_bin_base"
    local bin_rely_list="$OUT_DIR/BIN/list/${P}_bin_rely"
    local rpm_name_list="$OUT_DIR/RPM/list/${P}_rpm_name"
    local rpm_size_list="$OUT_DIR/RPM/list/${P}_rpm_size"
    local software_version=$(rpm -qpi $rpm_dir/$(head -n1 $rpm_list) | grep "^Version" | cut -d ":" -f 2 | sed 's/ //g')
    local software_name=$(rpm -qpi $rpm_dir/$(head -n1 $rpm_list) | grep "^Source RPM" | cut -d ":" -f 2 | sed -r 's/-[^-]+-[^-]+$//' | sed 's/ //g')
    echo -e "[${software_name}:${software_version}]\n\n\n" > $bin_base_list
    echo -e "[${software_name}:${software_version}]\n\n\n" > $bin_rely_list
    for r in $(cat $rpm_list); do
        local rpm_name=$(rpm -qpi $rpm_dir/$r | grep "^Name" | head -n1 | cut -d ":" -f 2)
        # prepare binary files
        local cpio_dir="$rpm_dir/${r}_cpio"
        mkdir -p $cpio_dir
        cd $cpio_dir
        rpm2cpio $rpm_dir/$r | cpio -id --quiet
        for f in $(rpm -qpl $rpm_dir/$r); do
            if [ "$f" == "(contains no files)" ]; then
                echo "WARNING: $r is empty"
                continue
            fi
            local f_name="${cpio_dir}${f}"
            local f_flag=$(ls -ld ${f_name} | cut -b 1)
            local f_perm=$(ls -ld ${f_name} | cut -b 1-10)
            local f_size=""
            local f_size_strip=""
            if [ "$f_flag" = "-" ]; then
                f_size=$(ls -ld ${f_name} | awk '{print $5}')
                if [ $? -eq 0 ]; then
                    strip ${f_name} 2>/dev/null
                    if [ $? -eq 0 ]; then
                        f_size_strip=$(ls -ld ${f_name} | awk '{print $5}')
                    fi
                fi
            fi
            echo "${rpm_name}:'${f}' \ ${f_perm} \ ${f_size} ${f_size_strip}" >> $bin_base_list
            file $f_name | grep ELF &>/dev/null
            if [ $? -eq 0 ]; then
                readelf -d $f_name | grep NEEDED | awk '{print $5}' > $rely_list
                sort $rely_list -o $rely_list
                for relyer in $(cat $rely_list); do
                    echo "${rpm_name}:'${f}' $relyer" >> $bin_rely_list
                done
            fi
        done
        cd - &>/dev/null
        # prepare rpm files
        echo "$rpm_name" >> $rpm_name_list
        local rpm_size=$(ls -l $rpm_dir/$r | awk '{print $5}')
        local rpm_size_flag="B"
        if [ "$rpm_size" -gt 1024 ]; then
            if [ "$rpm_size" -gt 1048576 ]; then
                if [ "$rpm_size" -gt 1073741824 ]; then
                    rpm_size=$(echo "scale=2;(${rpm_size}/1073741824)" | bc)
                    rpm_size_flag="G"
                else
                    rpm_size=$(echo "scale=2;(${rpm_size}/1048576)" | bc)
                    rpm_size_flag="M"
                fi
            else
                rpm_size=$(echo "scale=2;(${rpm_size}/1024)" | bc)
                rpm_size_flag="K"
            fi
        fi
        echo "${rpm_name}: ${rpm_size}${rpm_size_flag}" >> $rpm_size_list
    done
    sed -i 's/^ //' $bin_base_list $bin_rely_list
    sed -i 's/^ //' $rpm_name_list $rpm_size_list

    IFS=$OLD_IFS
}

compare() {
    # rpm name and size compare
    if [[ -f "$OUT_DIR/RPM/list/P1_rpm_name" && -f "$OUT_DIR/RPM/list/P2_rpm_name" ]]; then
        diff -Nurp $OUT_DIR/RPM/list/P1_rpm_name $OUT_DIR/RPM/list/P2_rpm_name > $OUT_DIR/RPM/change/rpm_name.txt
        if [ ! -s $OUT_DIR/RPM/change/rpm_name.txt ]; then
            echo "No rpm changed." >> $OUT_DIR/RPM/change/rpm_name.txt
        fi
    fi
    if [[ -f "$OUT_DIR/RPM/list/P1_rpm_size" && -f "$OUT_DIR/RPM/list/P2_rpm_size" ]]; then
        diff -Nurp $OUT_DIR/RPM/list/P1_rpm_size $OUT_DIR/RPM/list/P2_rpm_size > $OUT_DIR/RPM/change/rpm_size.txt
    fi

    # file rely, name and size compare
    if [[ -f "$OUT_DIR/BIN/list/P1_bin_rely" && -f "$OUT_DIR/BIN/list/P2_bin_rely" ]]; then
        diff -Nurp $OUT_DIR/BIN/list/P1_bin_rely $OUT_DIR/BIN/list/P2_bin_rely > $OUT_DIR/BIN/change/bin_rely.txt
    fi

    if [[ ! -f "$OUT_DIR/BIN/list/P1_bin_base" || ! -f "$OUT_DIR/BIN/list/P2_bin_base" ]]; then
        return 0
    fi
    diff -Nurp $OUT_DIR/BIN/list/P1_bin_base $OUT_DIR/BIN/list/P2_bin_base > $OUT_DIR/BIN/change/bin_base.txt

    # bin file
    awk '{print $1}' $OUT_DIR/BIN/list/P1_bin_base > $OUT_DIR/BIN/list/P1_bin_file
    awk '{print $1}' $OUT_DIR/BIN/list/P2_bin_base > $OUT_DIR/BIN/list/P2_bin_file
    diff -Nurp $OUT_DIR/BIN/list/P1_bin_file $OUT_DIR/BIN/list/P2_bin_file > $OUT_DIR/BIN/change/bin_file.txt

    # ignore the link-file and directory
    # format -> rpm_name:'f_name' \ f_perm \ f_size f_size_strip
    sed "/\\\ [ld]/d" $OUT_DIR/BIN/list/P1_bin_base | awk '{print $1" "$5" "$6}' | sed '1,4d' > $TMP_DIR/P1_file_size
    sed "/\\\ [ld]/d" $OUT_DIR/BIN/list/P2_bin_base | awk '{print $1" "$5" "$6}' | sed '1,4d' > $TMP_DIR/P2_file_size
    diff $TMP_DIR/P1_file_size $TMP_DIR/P2_file_size > $TMP_DIR/bin_size_ret
    if [ -s $TMP_DIR/bin_size_ret ]; then
        grep "<" $TMP_DIR/bin_size_ret | sed "s/^< //" > $TMP_DIR/P_1_size
        grep ">" $TMP_DIR/bin_size_ret | sed "s/^> //" > $TMP_DIR/P_2_size

        sort $TMP_DIR/P_1_size | awk '{print $1}' > $TMP_DIR/P_1_file
        sort $TMP_DIR/P_2_size | awk '{print $1}' > $TMP_DIR/P_2_file

        comm -12 $TMP_DIR/P_1_file $TMP_DIR/P_2_file > $TMP_DIR/P_name

        size_count() {
            local file="$1"
            local size="$2"
            local sign="$4"
            local strip_size_str=""
            if [ "$3" -ne "0" ]; then
                strip_size_str="($3)"
            fi
            if [ "$size" -gt "$SIZE_THRESHOLD" ]; then
                if [ "$size" -gt 1024 ]; then
                    size="$(echo "scale=3;(${size}/1024)" | bc)K"
                fi
                echo "$file  -------  ${sign}${size}${strip_size_str}" >> $OUT_DIR/BIN/change/bin_size.txt
            fi
        }
        for f in $(cat $TMP_DIR/P_name); do
            local p1_size=$(grep "${f}" $TMP_DIR/P_1_size | awk '{print $2}')
            local p2_size=$(grep "${f}" $TMP_DIR/P_2_size | awk '{print $2}')
            local p1_size_strip=$(grep "${f}" $TMP_DIR/P_1_size | awk '{print $3}')
            local p2_size_strip=$(grep "${f}" $TMP_DIR/P_2_size | awk '{print $3}')
            [ -z "$p1_size_strip" ] && p1_size_strip=0
            [ -z "$p2_size_strip" ] && p2_size_strip=0
            local size_change=0
            local size_strip_change=0
            if [ "$p1_size" -gt "$p2_size" ]; then
                size_change=$(($p1_size - $p2_size))
                size_strip_change=$(($p1_size_strip - $p2_size_strip))
                size_count $f $size_change $size_strip_change -
            else
                size_change=$(($p2_size - $p1_size))
                size_strip_change=$(($p2_size_strip - $p1_size_strip))
                size_count $f $size_change $size_strip_change +
            fi
        done
    fi
}

main() {
    local d1="$1"
    local d2="$2"
    if [[ $# -ne 2 || ! -d "$d1" || ! -d "$d2" ]]; then
        usage; exit
    fi
    init

    prepare "$d1" P1
    prepare "$d2" P2

    compare

    deinit
}

main "$@"

